/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.management.relation;

import javax.management.Notification;
import javax.management.ObjectName;

import java.io.InvalidObjectException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;

import java.security.AccessController;
import java.security.PrivilegedAction;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.sun.jmx.mbeanserver.GetPropertyAction;

import static com.sun.jmx.mbeanserver.Util.cast;

/**
 * A notification of a change in the Relation Service.
 * A RelationNotification notification is sent when a relation is created via
 * the Relation Service, or an MBean is added as a relation in the Relation
 * Service, or a role is updated in a relation, or a relation is removed from
 * the Relation Service.
 *
 * <p>The <b>serialVersionUID</b> of this class is <code>-6871117877523310399L</code>.
 *
 * @since 1.5
 */
@SuppressWarnings("serial")  // serialVersionUID not constant
public class RelationNotification extends Notification {

  // Serialization compatibility stuff:
  // Two serial forms are supported in this class. The selected form depends
  // on system property "jmx.serial.form":
  //  - "1.0" for JMX 1.0
  //  - any other value for JMX 1.1 and higher
  //
  // Serial version for old serial form
  private static final long oldSerialVersionUID = -2126464566505527147L;
  //
  // Serial version for new serial form
  private static final long newSerialVersionUID = -6871117877523310399L;
  //
  // Serializable fields in old serial form
  private static final ObjectStreamField[] oldSerialPersistentFields =
      {
          new ObjectStreamField("myNewRoleValue", ArrayList.class),
          new ObjectStreamField("myOldRoleValue", ArrayList.class),
          new ObjectStreamField("myRelId", String.class),
          new ObjectStreamField("myRelObjName", ObjectName.class),
          new ObjectStreamField("myRelTypeName", String.class),
          new ObjectStreamField("myRoleName", String.class),
          new ObjectStreamField("myUnregMBeanList", ArrayList.class)
      };
  //
  // Serializable fields in new serial form
  private static final ObjectStreamField[] newSerialPersistentFields =
      {
          new ObjectStreamField("newRoleValue", List.class),
          new ObjectStreamField("oldRoleValue", List.class),
          new ObjectStreamField("relationId", String.class),
          new ObjectStreamField("relationObjName", ObjectName.class),
          new ObjectStreamField("relationTypeName", String.class),
          new ObjectStreamField("roleName", String.class),
          new ObjectStreamField("unregisterMBeanList", List.class)
      };
  //
  // Actual serial version and serial form
  private static final long serialVersionUID;
  /**
   * @serialField relationId String Relation identifier of created/removed/updated relation
   * @serialField relationTypeName String Relation type name of created/removed/updated relation
   * @serialField relationObjName ObjectName {@link ObjectName} of the relation MBean of
   * created/removed/updated relation (only if the relation is represented by an MBean)
   * @serialField unregisterMBeanList List List of {@link ObjectName}s of referenced MBeans to be
   * unregistered due to relation removal
   * @serialField roleName String Name of updated role (only for role update)
   * @serialField oldRoleValue List Old role value ({@link ArrayList} of {@link ObjectName}s) (only
   * for role update)
   * @serialField newRoleValue List New role value ({@link ArrayList} of {@link ObjectName}s) (only
   * for role update)
   */
  private static final ObjectStreamField[] serialPersistentFields;
  private static boolean compat = false;

  static {
    try {
      GetPropertyAction act = new GetPropertyAction("jmx.serial.form");
      String form = AccessController.doPrivileged(act);
      compat = (form != null && form.equals("1.0"));
    } catch (Exception e) {
      // OK : Too bad, no compat with 1.0
    }
    if (compat) {
      serialPersistentFields = oldSerialPersistentFields;
      serialVersionUID = oldSerialVersionUID;
    } else {
      serialPersistentFields = newSerialPersistentFields;
      serialVersionUID = newSerialVersionUID;
    }
  }
  //
  // END Serialization compatibility stuff

  //
  // Notification types
  //

  /**
   * Type for the creation of an internal relation.
   */
  public static final String RELATION_BASIC_CREATION = "jmx.relation.creation.basic";
  /**
   * Type for the relation MBean added into the Relation Service.
   */
  public static final String RELATION_MBEAN_CREATION = "jmx.relation.creation.mbean";
  /**
   * Type for an update of an internal relation.
   */
  public static final String RELATION_BASIC_UPDATE = "jmx.relation.update.basic";
  /**
   * Type for the update of a relation MBean.
   */
  public static final String RELATION_MBEAN_UPDATE = "jmx.relation.update.mbean";
  /**
   * Type for the removal from the Relation Service of an internal relation.
   */
  public static final String RELATION_BASIC_REMOVAL = "jmx.relation.removal.basic";
  /**
   * Type for the removal from the Relation Service of a relation MBean.
   */
  public static final String RELATION_MBEAN_REMOVAL = "jmx.relation.removal.mbean";

  //
  // Private members
  //

  /**
   * @serial Relation identifier of created/removed/updated relation
   */
  private String relationId = null;

  /**
   * @serial Relation type name of created/removed/updated relation
   */
  private String relationTypeName = null;

  /**
   * @serial {@link ObjectName} of the relation MBean of created/removed/updated relation (only if
   * the relation is represented by an MBean)
   */
  private ObjectName relationObjName = null;

  /**
   * @serial List of {@link ObjectName}s of referenced MBeans to be unregistered due to relation
   * removal
   */
  private List<ObjectName> unregisterMBeanList = null;

  /**
   * @serial Name of updated role (only for role update)
   */
  private String roleName = null;

  /**
   * @serial Old role value ({@link ArrayList} of {@link ObjectName}s) (only for role update)
   */
  private List<ObjectName> oldRoleValue = null;

  /**
   * @serial New role value ({@link ArrayList} of {@link ObjectName}s) (only for role update)
   */
  private List<ObjectName> newRoleValue = null;

  //
  // Constructors
  //

  /**
   * Creates a notification for either a relation creation (RelationSupport
   * object created internally in the Relation Service, or an MBean added as a
   * relation) or for a relation removal from the Relation Service.
   *
   * @param notifType type of the notification; either: <P>- RELATION_BASIC_CREATION <P>-
   * RELATION_MBEAN_CREATION <P>- RELATION_BASIC_REMOVAL <P>- RELATION_MBEAN_REMOVAL
   * @param sourceObj source object, sending the notification.  This is either an ObjectName or a
   * RelationService object.  In the latter case it must be the MBean emitting the notification; the
   * MBean Server will rewrite the source to be the ObjectName under which that MBean is
   * registered.
   * @param sequence sequence number to identify the notification
   * @param timeStamp time stamp
   * @param message human-readable message describing the notification
   * @param id relation id identifying the relation in the Relation Service
   * @param typeName name of the relation type
   * @param objectName ObjectName of the relation object if it is an MBean (null for relations
   * internally handled by the Relation Service)
   * @param unregMBeanList list of ObjectNames of referenced MBeans expected to be unregistered due
   * to relation removal (only for removal, due to CIM qualifiers, can be null)
   * @throws IllegalArgumentException if: <P>- no value for the notification type <P>- the
   * notification type is not RELATION_BASIC_CREATION, RELATION_MBEAN_CREATION,
   * RELATION_BASIC_REMOVAL or RELATION_MBEAN_REMOVAL <P>- no source object <P>- the source object
   * is not a Relation Service <P>- no relation id <P>- no relation type name
   */
  public RelationNotification(String notifType,
      Object sourceObj,
      long sequence,
      long timeStamp,
      String message,
      String id,
      String typeName,
      ObjectName objectName,
      List<ObjectName> unregMBeanList)
      throws IllegalArgumentException {

    super(notifType, sourceObj, sequence, timeStamp, message);

    if (!isValidBasicStrict(notifType, sourceObj, id, typeName) || !isValidCreate(notifType)) {
      throw new IllegalArgumentException("Invalid parameter.");
    }

    relationId = id;
    relationTypeName = typeName;
    relationObjName = safeGetObjectName(objectName);
    unregisterMBeanList = safeGetObjectNameList(unregMBeanList);
  }

  /**
   * Creates a notification for a role update in a relation.
   *
   * @param notifType type of the notification; either: <P>- RELATION_BASIC_UPDATE <P>-
   * RELATION_MBEAN_UPDATE
   * @param sourceObj source object, sending the notification. This is either an ObjectName or a
   * RelationService object.  In the latter case it must be the MBean emitting the notification; the
   * MBean Server will rewrite the source to be the ObjectName under which that MBean is
   * registered.
   * @param sequence sequence number to identify the notification
   * @param timeStamp time stamp
   * @param message human-readable message describing the notification
   * @param id relation id identifying the relation in the Relation Service
   * @param typeName name of the relation type
   * @param objectName ObjectName of the relation object if it is an MBean (null for relations
   * internally handled by the Relation Service)
   * @param name name of the updated role
   * @param newValue new role value (List of ObjectName objects)
   * @param oldValue old role value (List of ObjectName objects)
   * @throws IllegalArgumentException if null parameter
   */
  public RelationNotification(String notifType,
      Object sourceObj,
      long sequence,
      long timeStamp,
      String message,
      String id,
      String typeName,
      ObjectName objectName,
      String name,
      List<ObjectName> newValue,
      List<ObjectName> oldValue
  )
      throws IllegalArgumentException {

    super(notifType, sourceObj, sequence, timeStamp, message);

    if (!isValidBasicStrict(notifType, sourceObj, id, typeName) || !isValidUpdate(notifType, name,
        newValue, oldValue)) {
      throw new IllegalArgumentException("Invalid parameter.");
    }

    relationId = id;
    relationTypeName = typeName;
    relationObjName = safeGetObjectName(objectName);

    roleName = name;
    oldRoleValue = safeGetObjectNameList(oldValue);
    newRoleValue = safeGetObjectNameList(newValue);
  }

  //
  // Accessors
  //

  /**
   * Returns the relation identifier of created/removed/updated relation.
   *
   * @return the relation id.
   */
  public String getRelationId() {
    return relationId;
  }

  /**
   * Returns the relation type name of created/removed/updated relation.
   *
   * @return the relation type name.
   */
  public String getRelationTypeName() {
    return relationTypeName;
  }

  /**
   * Returns the ObjectName of the
   * created/removed/updated relation.
   *
   * @return the ObjectName if the relation is an MBean, otherwise null.
   */
  public ObjectName getObjectName() {
    return relationObjName;
  }

  /**
   * Returns the list of ObjectNames of MBeans expected to be unregistered
   * due to a relation removal (only for relation removal).
   *
   * @return a {@link List} of {@link ObjectName}.
   */
  public List<ObjectName> getMBeansToUnregister() {
    List<ObjectName> result;
    if (unregisterMBeanList != null) {
      result = new ArrayList<ObjectName>(unregisterMBeanList);
    } else {
      result = Collections.emptyList();
    }
    return result;
  }

  /**
   * Returns name of updated role of updated relation (only for role update).
   *
   * @return the name of the updated role.
   */
  public String getRoleName() {
    String result = null;
    if (roleName != null) {
      result = roleName;
    }
    return result;
  }

  /**
   * Returns old value of updated role (only for role update).
   *
   * @return the old value of the updated role.
   */
  public List<ObjectName> getOldRoleValue() {
    List<ObjectName> result;
    if (oldRoleValue != null) {
      result = new ArrayList<ObjectName>(oldRoleValue);
    } else {
      result = Collections.emptyList();
    }
    return result;
  }

  /**
   * Returns new value of updated role (only for role update).
   *
   * @return the new value of the updated role.
   */
  public List<ObjectName> getNewRoleValue() {
    List<ObjectName> result;
    if (newRoleValue != null) {
      result = new ArrayList<ObjectName>(newRoleValue);
    } else {
      result = Collections.emptyList();
    }
    return result;
  }

  //
  // Misc
  //

  // Initializes members
  //
  // -param notifKind  1 for creation/removal, 2 for update
  // -param notifType  type of the notification; either:
  //  - RELATION_BASIC_UPDATE
  //  - RELATION_MBEAN_UPDATE
  //  for an update, or:
  //  - RELATION_BASIC_CREATION
  //  - RELATION_MBEAN_CREATION
  //  - RELATION_BASIC_REMOVAL
  //  - RELATION_MBEAN_REMOVAL
  //  for a creation or removal
  // -param sourceObj  source object, sending the notification. Will always
  //  be a RelationService object.
  // -param sequence  sequence number to identify the notification
  // -param timeStamp  time stamp
  // -param message  human-readable message describing the notification
  // -param id  relation id identifying the relation in the Relation
  //  Service
  // -param typeName  name of the relation type
  // -param objectName  ObjectName of the relation object if it is an MBean
  //  (null for relations internally handled by the Relation Service)
  // -param unregMBeanList  list of ObjectNames of MBeans expected to be
  //  removed due to relation removal
  // -param name  name of the updated role
  // -param newValue  new value (List of ObjectName objects)
  // -param oldValue  old value (List of ObjectName objects)
  //
  // -exception IllegalArgumentException  if:
  //  - no value for the notification type
  //  - incorrect notification type
  //  - no source object
  //  - the source object is not a Relation Service
  //  - no relation id
  //  - no relation type name
  //  - no role name (for role update)
  //  - no role old value (for role update)
  //  - no role new value (for role update)

  // Despite the fact, that validation in constructor of RelationNotification prohibit
  // creation of the class instance with null sourceObj its possible to set it to null later
  // by public setSource() method.
  // So we should relax validation rules to preserve serialization behavior compatibility.

  private boolean isValidBasicStrict(String notifType, Object sourceObj, String id,
      String typeName) {
    if (sourceObj == null) {
      return false;
    }
    return isValidBasic(notifType, sourceObj, id, typeName);
  }

  private boolean isValidBasic(String notifType, Object sourceObj, String id, String typeName) {
    if (notifType == null || id == null || typeName == null) {
      return false;
    }

    if (sourceObj != null && (
        !(sourceObj instanceof RelationService) &&
            !(sourceObj instanceof ObjectName))) {
      return false;
    }

    return true;
  }

  private boolean isValidCreate(String notifType) {
    String[] validTypes = {RelationNotification.RELATION_BASIC_CREATION,
        RelationNotification.RELATION_MBEAN_CREATION,
        RelationNotification.RELATION_BASIC_REMOVAL,
        RelationNotification.RELATION_MBEAN_REMOVAL};

    Set<String> ctSet = new HashSet<String>(Arrays.asList(validTypes));
    return ctSet.contains(notifType);
  }

  private boolean isValidUpdate(String notifType, String name,
      List<ObjectName> newValue, List<ObjectName> oldValue) {

    if (!(notifType.equals(RelationNotification.RELATION_BASIC_UPDATE)) &&
        !(notifType.equals(RelationNotification.RELATION_MBEAN_UPDATE))) {
      return false;
    }

    if (name == null || oldValue == null || newValue == null) {
      return false;
    }

    return true;
  }

  private ArrayList<ObjectName> safeGetObjectNameList(List<ObjectName> src) {
    ArrayList<ObjectName> dest = null;
    if (src != null) {
      dest = new ArrayList<ObjectName>();
      for (ObjectName item : src) {
        // NPE thrown if we attempt to add null object
        dest.add(ObjectName.getInstance(item));
      }
    }
    return dest;
  }

  private ObjectName safeGetObjectName(ObjectName src) {
    ObjectName dest = null;
    if (src != null) {
      dest = ObjectName.getInstance(src);
    }
    return dest;
  }

  /**
   * Deserializes a {@link RelationNotification} from an {@link ObjectInputStream}.
   */
  private void readObject(ObjectInputStream in)
      throws IOException, ClassNotFoundException {

    String tmpRelationId, tmpRelationTypeName, tmpRoleName;

    ObjectName tmpRelationObjName;
    List<ObjectName> tmpNewRoleValue, tmpOldRoleValue, tmpUnregMBeanList;

    ObjectInputStream.GetField fields = in.readFields();

    if (compat) {
      tmpRelationId = (String) fields.get("myRelId", null);
      tmpRelationTypeName = (String) fields.get("myRelTypeName", null);
      tmpRoleName = (String) fields.get("myRoleName", null);

      tmpRelationObjName = (ObjectName) fields.get("myRelObjName", null);
      tmpNewRoleValue = cast(fields.get("myNewRoleValue", null));
      tmpOldRoleValue = cast(fields.get("myOldRoleValue", null));
      tmpUnregMBeanList = cast(fields.get("myUnregMBeanList", null));
    } else {
      tmpRelationId = (String) fields.get("relationId", null);
      tmpRelationTypeName = (String) fields.get("relationTypeName", null);
      tmpRoleName = (String) fields.get("roleName", null);

      tmpRelationObjName = (ObjectName) fields.get("relationObjName", null);
      tmpNewRoleValue = cast(fields.get("newRoleValue", null));
      tmpOldRoleValue = cast(fields.get("oldRoleValue", null));
      tmpUnregMBeanList = cast(fields.get("unregisterMBeanList", null));
    }

    // Validate fields we just read, throw InvalidObjectException
    // if something goes wrong

    String notifType = super.getType();
    if (!isValidBasic(notifType, super.getSource(), tmpRelationId, tmpRelationTypeName) ||
        (!isValidCreate(notifType) &&
            !isValidUpdate(notifType, tmpRoleName, tmpNewRoleValue, tmpOldRoleValue))) {

      super.setSource(null);
      throw new InvalidObjectException("Invalid object read");
    }

    // assign deserialized vaules to object fields
    relationObjName = safeGetObjectName(tmpRelationObjName);
    newRoleValue = safeGetObjectNameList(tmpNewRoleValue);
    oldRoleValue = safeGetObjectNameList(tmpOldRoleValue);
    unregisterMBeanList = safeGetObjectNameList(tmpUnregMBeanList);

    relationId = tmpRelationId;
    relationTypeName = tmpRelationTypeName;
    roleName = tmpRoleName;
  }


  /**
   * Serializes a {@link RelationNotification} to an {@link ObjectOutputStream}.
   */
  private void writeObject(ObjectOutputStream out)
      throws IOException {
    if (compat) {
      // Serializes this instance in the old serial form
      //
      ObjectOutputStream.PutField fields = out.putFields();
      fields.put("myNewRoleValue", newRoleValue);
      fields.put("myOldRoleValue", oldRoleValue);
      fields.put("myRelId", relationId);
      fields.put("myRelObjName", relationObjName);
      fields.put("myRelTypeName", relationTypeName);
      fields.put("myRoleName", roleName);
      fields.put("myUnregMBeanList", unregisterMBeanList);
      out.writeFields();
    } else {
      // Serializes this instance in the new serial form
      //
      out.defaultWriteObject();
    }
  }
}
